class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        if (this.el) {
            //    1、先把真实DOM移入到内存当中fragment
            let fragment = this.nodeToFragment(this.el);
            //    2、编译 =》 提取想要的元素节点v-modal 和文本节点
            this.compile(fragment);
            //    3、把编译好的 fragment放到页面中
            this.el.appendChild(fragment)
        }
    }

//    辅助方法
    /**
     * 判断是否是元素节点
     * @param node
     * @returns {boolean}
     */
    isElementNode(node) {
        return node.nodeType === 1;
    }

    /**
     * 判断是否是指令
     * @param name
     */
    isDirective(name) {
        return name.includes('v-');
    }

//    核心方法

    /**
     * 编译文档碎片，插值。
     * @param fragment
     */
    compile(fragment) {
        let childNodes = fragment.childNodes;
        //  递归查找所有的子节点
        Array.from(childNodes).forEach((node) => {
            if (this.isElementNode(node)) { // 元素节点
                //    编译元素
                this.compileElement(node)
                //    递归调用compile方法
                this.compile(node);
            } else { // 文本节点
                this.compileText(node);
            }
        })
    }

    /**
     * 把真实DOM放到内存里面
     * @param el
     */
    nodeToFragment = (el) => {
        // 创建文档碎片
        let fragment = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            // appendChild方法是向节点添加最后一个子节点。
            // 如果是页面已存在的节点，就是将元素从一个元素移动到另一个元素中。
            fragment.appendChild(firstChild);
        }
        return fragment; // 内存中的节点
    }

    /**
     * 编译元素 带v-modal的元素
     * @param node
     */
    compileElement(node) {
        const attrs = node.attributes; // 获取属性
        Array.from(attrs).forEach((attr) => {
            //    判断属性名字是不是包含v-
            let attrName = attr.name
            if (this.isDirective(attrName)) {
            //    如果是指令，取到对应的值放到节点中。
            //    v-modal v-text v-html
                let expr = attr.value;
                // const type = attrName.slice(2);
                const [, type] = attrName.split('-');
                CompileUtil[type](node, this.vm,  expr);
            }
        })
    }

    /**
     * 编译文本节点 带插值符号的{{}}
     * @param node
     */
    compileText(node) {
        let text =  node.textContent; // 获取文本节点中的内容
        const reg = /\{\{([^}]+)\}\}/g // {{a}} {{b}}
        if (reg.test(text)) {
        //    this.vm.$data
             let expr = text.replace(reg, function () {
                return arguments[1]
            })
            expr = expr.trim();
            CompileUtil['text'](node, this.vm, expr);
        }
        // 使用matchAll
        // const matchedArr = [...text.matchAll(reg)];
        // if (matchedArr.length) {
        //     const expr = matchedArr[0][1];
        //     CompileUtil['text'](node, this.vm, expr);
        // }

    }
}

CompileUtil = {
    getVal(vm, expr) {
        const exprArr = expr.split('.')
        return  exprArr.reduce((pre, next) => {
            return pre[next];
        }, vm.$data);
    },

    getTextVal(vm, expr) {
        const exprArr = expr.split('.')
        let value = '';
        let tempObj = vm.$data;
        for (let i = 0; i < exprArr.length; i++) {
            tempObj = tempObj[exprArr[i]];
            if (!tempObj) break;
            if (i === exprArr.length -1) {
                value = tempObj
            }
        }
        return value;
    },

    text(node, vm, expr) { // 文本处理
        const updateFn = this.updater.textUpdater;
        let value = this.getTextVal(vm, expr);

        // 监听数据，数据变化了调用回调
        new Watcher(vm, expr, (newValue) => {
            // 当值变化后会调用cb，将新的值传递回来
            updateFn && updateFn(node, this.getTextVal(vm, expr));
        })

        updateFn && updateFn(node, value);
    },
    setVal(vm, expr, val) {
        const exprArr = expr.split('.');
        return exprArr.reduce((prev, next, currentIndex) => {
            if (currentIndex === exprArr.length - 1) {
                return prev[next] = val;
            }
            return  prev[next]
        }, vm.$data)
    },
    model(node, vm, expr) { // 输入框处理
        const updateFn = this.updater.modelUpdater;
        // 监听数据，数据变化了调用回调
        new Watcher(vm, expr, (newValue) => {
            // 当值变化后会调用cb，将新的值传递回来
            updateFn && updateFn(node, newValue);
        })
        node.addEventListener('input', (e) => {
            let newVal = e.target.value;
            this.setVal(vm, expr, newVal);
        });
        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    updater: {
        // 文本更新
        textUpdater(node, value) {
            node.textContent = value;
        },
        // 输入框更新
        modelUpdater(node, value) {
            node.value = value;
        }

    }
}